﻿//  -------------------- iSharePoint -------------------- //
// 
//  Company:	IT Complex, LLC
//  Project:	Smart.Documents
//  File name:	LookupFieldWithPicker.cs
//  Developer:	Solomatov Igor
//  Created:	16.04.2012
// 
//  -------------------- iSharePoint -------------------- //

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Xml.Linq;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;

// ReSharper disable CheckNamespace

namespace Smart.Documents.Fields.LookupFieldWithPicker
// ReSharper restore CheckNamespace
{
	public class LookupFieldWithPicker : SPFieldLookup
	{
		private const string SearchableFieldsPropName = "SearchableFields";
		private const string SearchQueryPropName = "SearchQuery";
		private const string TempDependentLookupsPropName = "tempDependentLookups";
		private const string CustomDefaultValuePropName = "CustomDefaultValue";
		private const string MaxSearchResultsPropName = "MaxSearchResults";
		private const string EntityEditorRowsPropName = "EntityEditorRows";
		private const string CustomVersionPropName = "CustomVersion";
		private const string CustomLookupListPropName = "CustomLookupList";
		private const string CustomLookupWebPropName = "CustomLookupWeb";

		private bool upgradingFlag;

		public new static readonly Version Version = new Version("1.1.0.0");

		internal SPFieldCollection fields;

		public LookupFieldWithPicker(SPFieldCollection fields, string fieldName)
			: base(fields, fieldName)
		{
			this.fields = fields;
		}

		public LookupFieldWithPicker(SPFieldCollection fields, string typeName, string displayName)
			: base(fields, typeName, displayName)
		{
			this.fields = fields;
		}

		public List<string> TempDependentLookups { get; set; }

		public override void OnUpdated()
		{
			if (upgradingFlag)
				return;

			UpgradeIfNecessary();

			EnsureCustomPropertiesFixIfNeeded();

			AddDependentLookups();
		}

		/// <summary>
		/// Hack: This hack is needed because custom field editors aren't able to persist custom properties
		/// </summary>
		private void EnsureCustomPropertiesFixIfNeeded()
		{
			// check if there is in a cached instance of this field from editor
			if (HttpContext.Current == null || !HttpContext.Current.Items.Contains(typeof (LookupFieldWithPicker).Name))
				return;

			var cachedInstance = (LookupFieldWithPicker) HttpContext.Current.Items[typeof (LookupFieldWithPicker).Name];

			// copy the properties to the new instance
			SearchableFields = cachedInstance.SearchableFields;
			CustomDefaultValue = cachedInstance.CustomDefaultValue;
			SearchQuery = cachedInstance.SearchQuery;
			TempDependentLookups = cachedInstance.TempDependentLookups;

			if (cachedInstance.MaxSearchResults > 0)
			{
				MaxSearchResults = cachedInstance.MaxSearchResults;
			}
			if (cachedInstance.EntityEditorRows > 0)
			{
				EntityEditorRows = cachedInstance.EntityEditorRows;
			}

			// remove the cached instance to avoid endless loops
			HttpContext.Current.Items.Remove(typeof (LookupFieldWithPicker).Name);

			// persist the properties
			Update(true);
		}

		public override void OnAdded(SPAddFieldOptions op)
		{
			EnsureCustomPropertiesFixIfNeeded();

			// without this all dependent fields would be created twice in list while adding a new site column to a site content type
			if (op != (SPAddFieldOptions.AddFieldInternalNameHint | SPAddFieldOptions.AddToNoContentType))
			{
				AddDependentLookups();
			}

			base.OnAdded(op);
		}

		private void AddDependentLookups()
		{
			lock (this)
			{
				if (TempDependentLookups == null)
					return;

				var dependentFieldNames = TempDependentLookups;
				var actualDependentFieldNames = GetDependentLookupInternalNames();

				foreach (var actualDependentFieldName in actualDependentFieldNames)
				{
					var actualDependentLookup = (SPFieldLookup) fields.GetFieldByInternalName(actualDependentFieldName);

					if (dependentFieldNames.Contains(actualDependentLookup.LookupField))
						continue;

					// if site column then remove a references to the site column from all site columns
					if (ParentList == null)
					{
						foreach (SPWeb web in fields.Web.Site.AllWebs)
						{
							// fresh site/web is needed because content type update concurrency issue
							using (var freshSite = new SPSite(web.Site.ID))
							{
								using (var freshWeb = freshSite.OpenWeb(web.ID))
								{
									var contentTypesToUpdate = new List<SPContentType>();

									foreach (SPContentType contentType in freshWeb.ContentTypes)
									{
										if (contentType.FieldLinks[actualDependentLookup.Id] == null)
											continue;

										contentType.FieldLinks.Delete(actualDependentLookup.Id);

										if (contentTypesToUpdate.All(c => c.Id != contentType.Id))
										{
											contentTypesToUpdate.Add(contentType);
										}
									}

									foreach (var spContentType in contentTypesToUpdate)
									{
										spContentType.Update(true);
									}
								}
							}

							web.Dispose();
						}
					}

					fields.Delete(actualDependentFieldName);
				}

				using (var lookupWeb = fields.Web.Site.OpenWeb(LookupWebId))
				{
					var lookupList = lookupWeb.Lists[new Guid(LookupList)];

					foreach (var fieldName in dependentFieldNames)
					{
						var alreadyExistingDependentLookupField =
							fields.OfType<SPFieldLookup>()
								.FirstOrDefault(f => f.PrimaryFieldId == Id.ToString() && f.LookupField == fieldName);

						if (alreadyExistingDependentLookupField != null)
							continue;

						var fieldInLookupList =
							lookupList.Fields.GetFieldByInternalName(fieldName);

						var projectedFieldName =
							fields.AddDependentLookup(Title + ":" + fieldInLookupList.Title, Id);

						var projectedField =
							(SPFieldLookup) fields.GetFieldByInternalName(projectedFieldName);

						projectedField.LookupField = fieldInLookupList.InternalName;

						projectedField.Update(true);
					}
				}
			}
		}

		private string _customDefaultValue;

		public string CustomDefaultValue
		{
			get
			{
				if (_customDefaultValue != null)
				{
					return _customDefaultValue;
				}

				var obj = GetCustomProperty(CustomDefaultValuePropName);

				if (obj == null)
				{
					return String.Empty;
				}

				_customDefaultValue = obj.ToString();

				return _customDefaultValue;
			}
			set
			{
				SetCustomProperty(CustomDefaultValuePropName, value ?? String.Empty);

				_customDefaultValue = value;
			}
		}

		private string _searchQuery;

		public string SearchQuery
		{
			get
			{
				if (_searchQuery != null)
				{
					return _searchQuery;
				}

				var obj = GetCustomProperty(SearchQueryPropName);

				if (obj == null)
				{
					return String.Empty;
				}

				_searchQuery = obj.ToString();

				return _searchQuery;
			}
			set
			{
				SetCustomProperty(SearchQueryPropName, value ?? String.Empty);

				_searchQuery = value;
			}
		}

		private int _entityEditorRows;

		public int EntityEditorRows
		{
			get
			{
				if (_entityEditorRows > 0)
				{
					return _entityEditorRows;
				}

				var obj = GetCustomProperty(EntityEditorRowsPropName);

				if (String.IsNullOrEmpty(obj as String))
					return 1;

				var str = obj.ToString();
				int result;
				int.TryParse(str, out result);

				_entityEditorRows = result;

				return _entityEditorRows;
			}
			set
			{
				if (value < 1)
					throw new Exception("EntityEditorRows must be greater or equals than 1");

				SetCustomProperty(EntityEditorRowsPropName, value.ToString());

				_entityEditorRows = value;
			}
		}

		private int _maxSearchResults;

		public int MaxSearchResults
		{
			get
			{
				if (_maxSearchResults > 0)
				{
					return _maxSearchResults;
				}

				var obj = GetCustomProperty(MaxSearchResultsPropName);

				if (String.IsNullOrEmpty(obj as String))
					return 100;

				var str = obj.ToString();
				int result;
				int.TryParse(str, out result);

				_maxSearchResults = result;

				return _maxSearchResults;
			}
			set
			{
				if (value < 1)
					throw new Exception("MaxSearchResults must be a positive number");

				SetCustomProperty(MaxSearchResultsPropName, value.ToString());

				_maxSearchResults = value;
			}
		}

		private List<string> _searchableFields;

		public List<string> SearchableFields
		{
			get
			{
				if (_searchableFields != null)
				{
					return _searchableFields;
				}

				var strSearchFields = GetCustomProperty(SearchableFieldsPropName) as string;

				_searchableFields = new List<string>();

				if (!string.IsNullOrEmpty(strSearchFields))
				{
					_searchableFields = new List<string>(strSearchFields.Split(','));
				}
				else
				{
					// if no search field is defined use the lookup field as default
					_searchableFields.Add(LookupField);
				}

				return _searchableFields;
			}
			set
			{
				if (value.Count == 0)
					return;

				var str = string.Empty;

				foreach (var strField in value)
				{
					if (str.Length > 0)
						str += ",";

					str += strField;
				}

				SetCustomProperty(SearchableFieldsPropName, str);

				_searchableFields = value;
			}
		}

		public override bool AllowMultipleValues
		{
			get { return base.AllowMultipleValues; }
			set
			{
				base.AllowMultipleValues = value;

				EntityEditorRows =
					value && EntityEditorRows == 1
						? 2
						: value || EntityEditorRows != 2
						  	? EntityEditorRows
						  	: 1;

				SetFieldAttribute("Type", value ? "LookupFieldWithPickerMulti" : "LookupFieldWithPicker");
			}
		}

		public override BaseFieldControl FieldRenderingControl
		{
			get
			{
				BaseFieldControl control = AllowMultipleValues ? new MultiLookupWithPickerControl() : new LookupFieldWithPickerControl();

				control.FieldName = InternalName;

				return control;
			}
		}

		internal void SetFieldAttribute(string attribute, string value)
		{
			//Hack: Invokes an internal method from the base class
			var baseType = typeof (LookupFieldWithPicker);
			var mi = baseType.GetMethod("SetFieldAttributeValue", BindingFlags.Instance | BindingFlags.NonPublic);
			mi.Invoke(this, new object[] {attribute, value});
		}

		internal string GetFieldAttribute(string attribute)
		{
			//Hack: Invokes an internal method from the base class
			var baseType = typeof (LookupFieldWithPicker);
			var mi = baseType.GetMethod("GetFieldAttributeValue", BindingFlags.Instance | BindingFlags.NonPublic, null, new[] {typeof (String)}, null);
			var obj = mi.Invoke(this, new object[] {attribute});

			return obj == null ? "" : obj.ToString();
		}

		public void UpgradeIfNecessary()
		{
			if (!IsInCompatibilityMode)
				return;

			upgradingFlag = true;

			var xDoc = XDocument.Parse(SchemaXml);
			var xField = xDoc.Root;

			RemoveAttribute(xField, TempDependentLookupsPropName);
			RemoveAttribute(xField, CustomDefaultValuePropName);
			RemoveAttribute(xField, SearchQueryPropName);
			RemoveAttribute(xField, MaxSearchResultsPropName);
			RemoveAttribute(xField, EntityEditorRowsPropName);
			RemoveAttribute(xField, SearchableFieldsPropName);

			SchemaXml = xDoc.ToString();

			//important: call base.SetCustomProperty to bypass compatibility mode
			base.SetCustomProperty(CustomVersionPropName, Version.ToString());

			Update(true);

			upgradingFlag = false;
		}

		private static void RemoveAttribute(XElement xField, string name)
		{
			var xAttribute = xField.Attribute(name);

			if (xAttribute != null)
				xAttribute.Remove();
		}

		public Version CurrentInstanceVersion
		{
			get
			{
				//base.GetCustomVersion is import to avoid endless loop
				var versionString = base.GetCustomProperty(CustomVersionPropName) as String;

				return
					String.IsNullOrEmpty(versionString)
						? new Version("1.0.0.0")
						: new Version(versionString);
			}
		}

		public bool IsInCompatibilityMode
		{
			get { return (CurrentInstanceVersion < Version); }
		}

		public new object GetCustomProperty(string name)
		{
			return
				IsInCompatibilityMode
					? GetFieldAttribute(name)
					: base.GetCustomProperty(name);
		}

		public new void SetCustomProperty(string name, object value)
		{
			if (IsInCompatibilityMode)
			{
				SetFieldAttribute(name, value as String);
			}

			base.SetCustomProperty(name, value);
		}
	}
}